function calc_no_parentheses(exp) {
    var fn = { '+': (a, b) => a + b, '*': (a, b) => a * b, '/': (a, b) => b / a, '%': (a, b) => b % a };
    //trim head +
    exp = exp.replace(/^\+*/, '').replace(/\s/g, '');
    //all sub as plus negative number
    exp = exp.replace(/(\d+)\-(\d+)/g, (_, a, b) => a + '+-' + b);
    var nums = [];
    var opers = [];
    for (x of exp.match(/(-?\d+(\.\d+)?)|[+*/%]/g)) {
        if (/[*/%]/.test(x)) {
            if (opers.length && opers[opers.length - 1] != '+') nums.push(fn[opers.pop()](nums.pop(), nums.pop()));
            opers.push(x);
        } else if (x === '+') {
            if (opers.length === 0) {
                opers.push(x);
            } else {
                while (opers.length) nums.push(fn[opers.pop()](nums.pop(), nums.pop()));
                opers.push(x);
            }
        } else {
            nums.push(+x);
        }
    }
    while (opers.length) nums.push(fn[opers.pop()](nums.pop(), nums.pop()));
    return nums[0];
}
function Interpreter() {
    this.vars = {};
    this.functions = {};
}
Interpreter.prototype.has_declared_vars = function (k) {
    return Object.keys(this.vars).includes(k);
}

Interpreter.prototype.has_declared_fun = function (k) {
    return Object.keys(this.functions).includes(k);
}

Interpreter.prototype.invalid = function (exp) {
    if (/^\d+\s+\d+/.test(exp)) return true
    if (/\d+[a-z]+/.test(exp)) return true;
    return false;
}
Interpreter.prototype.basic = function (exp) {
    //caculate normal math expression
    exp = exp.replace(/\-\-/g, '+').replace(/(\+\-)|(\-\+)/g, '-');
    while (/\([^()]+\)/.test(exp)) {
        exp = exp.replace(/\(([^()]+)\)/,
            (_, exp) => calc_no_parentheses(exp)
        );
        exp = exp.replace(/\-\-/g, '+').replace(/(\+\-)|(\-\+)/g, '-').replace(/\*\+/g, '*').replace(/\/\+/g, '/');
    }
    return calc_no_parentheses(exp);
}
Interpreter.prototype.def_f = function (expr) {
    if (!/^fn\s([a-z]+)\s(([a-z]\s)*)=>(.+)$/.test(expr)) throw 'Error:invalid expression!';
    var fn_name = RegExp.$1;
    var args = RegExp.$2.trim().split(' ').filter(v => v.trim() != '');
    var code = RegExp.$4;
    //check if function name has been declared as variable
    if (this.has_declared_vars(fn_name)) throw 'Error:function name has been declared as variable';
    //check if exist illegal arguments
    if (args.length > new Set(args).size) throw 'Error:exist duplicated arguments';
    if ([...new Set(code.match(/[a-z]/g) || [])].sort().join('') != ([].concat(args)).sort().join(''))
        throw 'Error:contains invalid variable names';
    //store function define
    this.functions[fn_name] = { 'arguments': args, 'code': code };
    //console.log(`function ${fn_name}(${args.join(',')}) { return ${code};} `)
    return ''
}
Interpreter.prototype.variable = function (expr) {

}
Interpreter.prototype.call_fun = function (expr) {
    if (/^([a-z]+)\s(.*)$/.test(expr)) {
        var fn_name = RegExp.$1;
        var args = RegExp.$2.trim().split(' ');
        console.log(`call ${fn_name}(${args.join(',')})`)
        if (!this.has_declared_fun(fn_name)) throw 'Error:function donot exist';
        var f = this.functions[fn_name];
        if (args.length != f.arguments.length) throw 'Error:arguments length donot match';
        var exp = f.code;
        for (var i = 0; i < f.arguments.length; i++) {
            exp = exp.replace(new RegExp(f.arguments[i], 'g'), args[i]);
        }
        return this.input(exp);
    }
}

Interpreter.prototype.input = function (expr) {
    console.log(expr)
    if (this.invalid(expr)) throw 'Error:invalid expression!';
    if (/^\s*$/.test(expr)) return '';
    if (expr.includes('fn')) { return this.def_f(expr); }
    else {
        while (/\([^()]+\)/.test(expr)) {
            expr = expr.replace(/\(([^()]+)\)/,
                (_, exp) => this.input(exp)
            );
        }
    }
    //only digits
    if (!/[a-z]/.test(expr)) { return this.basic(expr); }
    else if (expr.includes('=')) {
        var arr = expr.split('=');
        var v = arr.pop().trim();
        if (/^[a-z]$/.test(v)) {
            if (!this.has_declared_vars(v)) throw 'Error:variable donot exist';
            v = this.vars[v];
        } else if (/[+\-*/%]/.test(v)) {
            v = this.input(v);
        }
        while (arr.length) {
            var k = arr.pop().trim();
            //if (this.has_declared_vars(k)) throw 'Error:variable has been declared';
            if (this.has_declared_fun(k)) throw 'Error:variable has been declared';
            this.vars[k] = +v;
        }
        return +v;
    } else if (/^[a-z]+$/.test(k = expr.replace(/\s/g, ''))) {

        if (!this.has_declared_vars(k) &&
            !this.has_declared_fun(k)) throw 'Error:variable or function donot exist';
        if (this.has_declared_vars(k)) return this.vars[k];
        if (this.has_declared_fun(k)) {
            var f = this.functions[k];
            console.log(f.arguments)
            if (f.arguments.length) throw 'Error:invalid argument';
            return this.input(f.code);
        }
    } else if (/[a-z]\s[+\-*/%]/.test(expr)) {
        expr = expr.replace(/[a-z]/g, (v) => {
            if (!this.has_declared_vars(v)) throw 'Error:variavle did not exist!'; return this.vars[v];
        });
        return this.input(expr);
    } else {
        var arr = expr.split(' ');
        var funs = arr.filter(v => this.has_declared_fun(v));
        if (funs.length > 1)//chain functions
        {
            var arr0 = []
            for (var i = 1; i < arr.length; i++) {
                if (this.has_declared_fun(arr[i])) {
                    if (arr0.length) {
                        var exp = arr0.join(' ');
                        expr = expr.replace(new RegExp(exp), this.input(exp));
                        arr0 = [];
                    }
                    arr0.push(arr[i]);
                } else {
                    arr0.push(arr[i]);
                }
            }
            var exp = arr0.join(' ');
            expr = expr.replace(new RegExp(exp), this.input(exp));
            return this.input(expr);
        } else {//single function call
            return this.call_fun(expr);
        }
    }
};
